/* CsJni - .NET / Java Bridge
 * 
 * Copyright 2010 Robert E. Burke Licensed under the Apache License, Version 
 * 2.0 (the "License"); you may not use this file except in compliance with 
 * the License. You may obtain a copy of the License at 
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 
 * law or agreed to in writing, software distributed under the License is 
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
 * KIND, either express or implied. See the License for the specific language 
 * governing permissions and limitations under the License. 
*/

using System;
using System.Collections.Generic;
using System.Text;

namespace CsJni
{
    // NOTES ON JniLocalFrame.
    //
    // This class is very important.  It deals with two garbage collectors, the Java and .NET GCs.
    // Some important points:
    //
    // 1) Typical JNI code involves Java calling native code (J2N).  In a J2N call, the JVM 
    //      reserves 16 memory slots for Java objects created/referenced in the context of the native call.  
    //      When the native call returns, the local references are automatically freed, which allows 
    //      the corresponding Java objects to be GCed (if there are also no Java reference to them).
    //
    // 2) In a J2N call, if you need more than 16 local references, you can get more with 
    //      EnsureLocalCapacity (poor choice) or PushLocalFrame/PopLocalFrame (much better choice).  
    //      Usually, 16 local references is plenty so you do not need to worry about Java memory 
    //      concerns too much.
    //
    // 3) Some native applications (such as this) create a JVM and call from native code to Java (N2J).  
    //     In an N2J call, no entity keeps track of the local references, so it is absolutely imperative 
    //     that the native code explicitly free all local references.
    //
    // 4) In N2J code, it may be feasible for local references to be held onto for a long time and 
    //     used in several methods.  This is a bad idea, because they should be treated the same as 
    //     local references in J2N calls, which are local to the scope of the native method.  Local 
    //     references are also local to a particular thread within the JVM.  See class JniEnvironmentCache 
    //     for notes on Java and .NET threading issues.
    //
    // 5) It is a bad idea to try to clean up local references to Java objects in a C# finalizer.  This is 
    //     because the finalizer runs on the .NET GC thread, which is almost certainly a different thread 
    //     than the local reference was created on.   DeleteLocalRef is one of the JNI calls which is 
    //     safe to make with a pending exception, and it never seems to raise an exception of its own, 
    //     so calling DeleteLocalRef from a thread other than the thread it was created 
    //     on will not cause a JNI exception, but will certainly cause a Java memory leak.  Note that 
    //     CsJni global references can be used across more than one thread attached to the JVM, so it is 
    //     quite possible to call DeleteGlobalRef from a C# finalizer, so long as you attach the .NET 
    //     GC thread to the JVM.
    //
    // 6) A second reason it is bad to clean up local references to Java objects in a C# finalizer is the 
    //     "problem" in .NET of nondeterministic finalization.  Basically, it is very possible that even 
    //     within the scope of a single function, a C# object that is no longer referenced can be GCed.  
    //     See the MSDN documentation on GC.KeepAlive for more details.
    //
    // 7) This class (JniLocalFrame) provides explicit clean up of local Java references.  Not using this 
    //     class WILL cause runtime exceptions.  To guarantee that local references are indeed cleaned up 
    //     properly, this class cooperates with JniObject and JniEnvironment to track the C# wrapper classes 
    //     (JniObject and its subclasses) which wrap the JNI local reference handles.
    //
    // 8) This class should always be used with try/finally.  The JniLocalFrame references should be 
    //     declared before the try block, and JniLocalFrame.SafePop should always be called in the finally 
    //     block.  Failure to do this will leak Java memory and will cause this library to throw .NET exceptions.
    //
    // 9) If you need to return a JniObject from the scope of a JniLocalFrame, use Pop.  You should still 
    //     have a call to SafePop in the finally block in case of exceptions.
    //
    // 10) JniLocalFrame, like JniEnvironment, should never be used across more than one thread.  In fact, 
    //      JniLocalFrame should never be used outside the method it is declared in.
    //
    // **************************************************************************************************
    // **************************************************************************************************
    //
    // The following example shows the correct usage of JniLocalFrame:
    //
    //    JniLocalFrame frame = environment.CreateLocalFrame(3);
    //    try
    //    {
    //        // do stuff, optionally return a JniObject, in this example, a string
    //        JniString myReturn = ....
    //        return (JniString)frame.Pop(myReturn);
    //    }
    //    finally
    //    {
    //        frame.SafePop(); // ALWAYS CALL FROM FINALLY BLOCK!
    //    }
    //
    // **************************************************************************************************
    // **************************************************************************************************

    public sealed class JniLocalFrame
    {
        private JniEnvironment mEnvironment;
        private bool mPopped = false;
        private int mCapacity;
        private LinkedList<JniObject> mTrackedObjects;
        // NOTE: need a slight fudge factor to handle exceptions, the Throwable *may* 
        // get created in the context of *this* frame.
        private const int CAPACITY_FUDGE = 2;

        internal JniLocalFrame(JniEnvironment env, int capacity)
        {
            mEnvironment = env;
            mCapacity = capacity;
            int result = JniEnvironment.Functions.PushLocalFrame(mEnvironment.Handle, mCapacity + CAPACITY_FUDGE);
            if (0 != result)
            {
                JniException.CheckErrorCode(mEnvironment, result);
            }
            // EXCEPTION CHECK: Do nothing, already checked for exception above.
            mTrackedObjects = new LinkedList<JniObject>();
            mEnvironment.PushFrame(this);
        }

        ~JniLocalFrame()
        {
            if (!mPopped)
            {
                // TODO - complain that frame was not popped
                //throw new JniException("TODO"); // not good to raise exceptions from finalizers
                JavaVM.Log("TODO");
            }
        }

        public void SafePop()
        {
            if (mPopped)
            {
                return;
            }
            Pop(null);
        }

        public JniObject Pop(JniObject resultIn)
        {
//#if (DEBUG)
            // Sanity check that this JniLocalFrame is not being used across threads
            if (!JniEnvironmentCache.CheckThreadAffinity(mEnvironment))
            {
                throw new JniException("TODO");
            }
//#endif
            if (mPopped)
            {
                throw new JniException("Cannot pop more than once");
            }
            mPopped = true;

            // TODO - fix here because we may need to clear pending exceptions
            // In some versions of the JNI, not safe to call PopLocalFrame
            // with a pending exception.
            // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6369169

            // NOTE: OK if "JniObject result" is null
            IntPtr safeResult = ((resultIn == null) ? IntPtr.Zero : resultIn.Handle);
            IntPtr callResult = JniEnvironment.Functions.PopLocalFrame(mEnvironment.Handle, safeResult);
            PopInternal(resultIn);
            JniThrowable.CheckException(mEnvironment);
            JniObject resultOut = (resultIn == null) ? null : 
                JniObject.CreateObjectOfType(mEnvironment, callResult, resultIn);
            GC.SuppressFinalize(this);
            return resultOut;
        }

        internal void Add(JniObject jniObject)
        {
            if (mTrackedObjects.Count >= (mCapacity + CAPACITY_FUDGE))
            {
                throw new JniException("TODO");
            }
            mTrackedObjects.AddLast(jniObject);
        }

        internal void Remove(JniObject jniObject)
        {
            mTrackedObjects.Remove(jniObject);
        }

        private void PopInternal(JniObject resultIn)
        {
            mEnvironment.PopFrame(this);
            if (resultIn != null)
            {
                mTrackedObjects.Remove(resultIn);
                mEnvironment.AddToCurrentFrame(resultIn);
            }
            foreach (JniObject tracked in mTrackedObjects)
            {
                tracked.PopRelease();
            }
            mTrackedObjects.Clear();
        }
    }
}
